# -*- coding: utf-8 -*-
#
# Copyright 2008 Yejun Yang
#

import logging

class Error(Exception):
    pass
class NetworkError(Error):
    pass

class Table(dict):
    __slots__ = ['name','expires']
    _tables = {}
    def __new__(cls, name, expires=0, *args, **kwargs):
        if name in Table._tables:
            return Table._tables[name]
        Table._tables[name] = t = super(Table, cls).__new__(cls, *args, **kwargs)
        return t
        
    def __init__(self, name, expires=0, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self.name = name
        self.expires = expires
    def __repr__(self):
        return self.name+":"+dict.__repr__(self)
        

from webappex import Handler
class LocalTable(Table):
    _cid = {}
    def __init__(self, name, expires=0, *args, **kwargs):
        if Handler._count != LocalTable._cid.get(name,0):
            self.clear()
            LocalTable._cid[name] = Handler._count
        Table.__init__(self, name, expires, *args, **kwargs)
            
from google.appengine.api import memcache

class MemcacheTable(LocalTable):
    def _key(self, key):
        return "table:%s:%s" % (self.name, key) 
        
    def __setitem__(self, key, value):
        logging.info("mem set")
        if not memcache.set(self._key(key), value, time=self.expires):
            raise Error("memcache error")
        LocalTable.__setitem__(self, key, value)
    
    def __getitem__(self, key):
        if LocalTable.has_key(self, key):
            return LocalTable.__getitem__(self, key)
        value = memcache.get(self._key(key))
        logging.info("mem get")
        if value is None:
            raise KeyError(key)
        LocalTable.__setitem__(self, key, value)
        return value
        
    def get(self, key, d=None, use_cache=True):
        if use_cache and LocalTable.has_key(self, key):
            return LocalTable.__getitem__(self, key)
        value = memcache.get(self._key(key))
        logging.info("mem get")
        if value is None:
            return d
        if use_cache: 
            LocalTable.__setitem__(self, key, value)
        return value
            
    def setdefault(self, key, d=None, use_cache=True):
        if use_cache and LocalTable.has_key(self, key):
            return LocalTable.__getitem__(self, key)
        value = memcache.get(self._key(key))
        logging.info("mem get")
        if value is None:
            value = d
            MemcacheTable.__setitem__(self, key, value)
        elif use_cache:
            LocalTable.__setitem__(self, key, value)
        return value        
    
    def __delitem__(self, key):
        if LocalTable.has_key(self, key):
            LocalTable.__delitem__(self, key)
        res = memcache.delete(self._key(key))
        logging.info("mem del")
        if res == memcache.DELETE_ITEM_MISSING: raise KeyError(key)
        if res == memcache.DELETE_NETWORK_FAILURE: raise NetworkError(key)

    
    def __contains__(self, key):
        try:
            value = MemcacheTable.__getitem__(self,key)
        except KeyError:
            return False
        return True
        
    has_key = __contains__
    
    def update(self, E, **F):
        map = {}
        map.update(E, **F)
        memcache.set_multi(map, time=self.expires, key_prefix=self._key(""))
        logging.info("mem multi set")
        LocalTable.update(self, E, **F)
    
from google.appengine.ext import db
import pickle, datetime, time

class DatastoreTable(MemcacheTable):
    __slots__ = ['kind']
    _kinds = {}
    def __init__(self, name, expires=0, *args, **kwargs):
        MemcacheTable.__init__(self, name, expires, *args, **kwargs)
        if name not in DatastoreTable._kinds:
            if expires:
                DatastoreTable._kinds[name] = type("table_"+name, (db.Model,), dict(data=db.BlobProperty(),atime=db.DateTimeProperty(auto_now=True)))
            else:
                DatastoreTable._kinds[name] = type("table_"+name, (db.Model,), dict(data=db.BlobProperty(),ctime=db.DateTimeProperty(auto_now_add=True)))
        self.kind = DatastoreTable._kinds[name]
    
    def _enc(self, data):
        return pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
    
    def _dec(self, data):
        return pickle.loads(data)
    
    def _key_d(self, key):
        return "k"+key
        
    def expired(self, i):
        if self.expires == 0:
            return False
        expire_time = datetime.datetime.now() - datetime.timedelta(seconds=self.expires)
        return i.atime < expire_time
        
    def __setitem__(self, key, value):
        logging.info("datastore set")
        self.kind(key_name=self._key_d(key), data=self._enc(value)).put()
        MemcacheTable.__setitem__(self, key, value)
        self.cleanup()
        
    def __getitem__(self, key):
        if MemcacheTable.has_key(self, key):
            return MemcacheTable.__getitem__(self, key)
        i = self.kind.get_by_key_name(self._key_d(key))
        if i is not None and not self.expired(i):
            value = self._dec(i.data)
        else:
            raise KeyError(key)
        MemcacheTable.__setitem__(self, key, value)
        return value        
        
    def get(self, key, d=None, use_cache=True):
        if use_cache and MemcacheTable.has_key(self, key):
            return MemcacheTable.__getitem__(self, key)
        i = self.kind.get_by_key_name(self._key_d(key))
        if i is not None and not self.expired(i):
            value = self._dec(i.data)
        else: 
            value = d
        if use_cache:
            MemcacheTable.__setitem__(self, key, value)
        return value
            
    def setdefault(self, key, d=None, use_cache=True):
        if use_cache and MemcacheTable.has_key(self, key):
            return MemcacheTable.__getitem__(self, key)
        i = self.kind.get_by_key_name(self._key_d(key))
        if i is not None and not self.expired(i):
            value = self._dec(i.data)
            if use_cache:
                MemcacheTable.__setitem__(self, key, value)
        else: 
            value = d
            DatastoreTable.__setitem__(self, key, value)
        return value  
        
    def __delitem__(self, key):
        try:
            MemcacheTable.__delitem__(self, key)
        except KeyError:
            pass
        i = self.kind.get_by_key_name(self._key_d(key))
        if i is not None:
            i.delete()
        else:
            raise KeyError(key)
            
    def __contains__(self, key):
        try:
            value = DatastoreTable.__getitem__(self, key)
        except KeyError:
            return False
        return True
        
    has_key = __contains__
    
    _last_cleanup = 0
    _cleanup_time = 100
    _cleanup_size = 50
    def cleanup(self, forced=False):
        now = time.time()
        if not forced and DatastoreTable._cleanup_time > now - DatastoreTable._last_cleanup:
            return
        expire_time = datetime.datetime.now() - datetime.timedelta(seconds=self.expires+DatastoreTable._cleanup_time)
        db.delete(self.kind.all().filter("atime<",expire_time).fetch(DatastoreTable._cleanup_size))
        DatastoreTable._last_cleanup = now
        
    def update(self, E, **F):
        map = {}
        map.update(E, **F)
        for x in map:
            DatastoreTable.__setitem__(self, x, map[x])
        MemcacheTable.update(map)
        
        